# -*- coding: utf-8 -*-
# ========================================================================================
# Tortuga Content Management System
#
# Basic API Usage
#
# The goal with this annotated script is to guide you, the developer, through some basic
# tasks using the core Tortuga API.  We'll create a template... make a page that uses that
# template.... add some gadgets to that page... publish that page through the workflow
# system... make some new revisions of the page and then undo them... and finally
# we'll see how easy it is to add different translations of that page.   
# 
# These examples assume a basic knowlege of SQLAlchemy.  Nothing fancy, I assure you,
# just a basic knowlege of how to add objects to a session and flush them to the
# database.  Want a primer?  session.add(some_object) and session.flush() are all
# we're talking about.  Not too bad, right?
# 
# Please keep two things in mind.  The first is DO NOT BE FOOLED by how easy it will be
# to accomplish these examples.  It took over a year of constant development to get the
# Tortuga core to the point where it's powerful yet easy to work with.  The second thing
# to keep in mind is that you're just looking at a preview of the 'core' functionality,
# mainly the models.  Creating revisioned, multi-lingual, pluggable content models 
# was the biggest obstacle to moving forward on this project.  I hope you'll agree
# this solution is rock-solid and we're ready to move forward on the rest of the code.
# ========================================================================================

import datetime

from sqlalchemy import and_

from tortuga import database
from tortuga.models import Page, Template, BasicContentGadget, WORKFLOW_STATUS, CONTENT_STATUS
from tortuga.render import render

# ===========================
# Database setup for test use
# ===========================
engine = database.get_test_engine()
database.bind(engine)
database.create_tables()
session = database.session


# =======================================================================================
# The first thing you'll want to do in a Content Management System is to 
# create a Template or two to use for your pages.
# =======================================================================================

# Templates may be specified by a file-path...
t1 = Template(title=u"Super Template", engine=u'genshi', path=u"/here/is/the/template.html", code=None)

# Templates may also be specified by the actual code.  Notice the template is trying to
# render a container named 'main_content_area'.  The basic idea is that the person
# designing the templates will choose areas in the template where content creators
# can place 'gadgets' such as a WYSIWYG editor, a news feed, photo gallery, etc.  
# So there's two terms here, Gadgets and Containers.  Remember those!
t2 = Template(title=u"Main Template", engine=u'genshi', path=None, code=u"""
<html xmlns="http://www.w3.org/1999/xhtml">
  <head><title>${title}</title></head>
  <body>${tortuga.render_container('main_content_area')}</body>
</html>
""")

# Of course we have to tell SQLAlchemy to save our objects to the database:
session.add(t1)
session.add(t2)
session.flush()


# =======================================================================================
# Next you'll want to create a Page using one of our templates
# =======================================================================================

# Creating a page is incredibly easy and intuitive!
contact_us = Page(title=u"Contact Us", path_name=u"contact_us", template=t2)
session.add(contact_us)
session.flush()

# NOTE:
# You might be thinking, "how do I specify the metadata?" or "How do I specify the 
# navigation name?" or all these questions.  I specifically left that stuff out of 
# this code preview, so we can just focus on how all the pieces fit together.  
# The release version of Tortuga will of course have all the expected page attributes.

# Anyways... back to our tasks.
# Remember in our 't2' template, we defined a Container called 'main_content_area' ???
# This is a place we'll be able to drag and drop Gadgets like WYSIWYG editors,
# news feeds, and so on.  Let's add a Gadget to our page that renders basic html content.
my_first_gadget = BasicContentGadget(text=u"<p>Our Phone: 555-1212</p>")
contact_us.containers['main_content_area'].gadgets.append(my_first_gadget)

# Let's make sure the gadget/container/page SQLAlchemy relationship is working:
assert my_first_gadget in session
session.flush()

# Rendering a page is also very easy.  The 'rendering' machinery is just a baby right now,
# but now that the models are strong, this is the next thing to do.  I doubt this
# will be the final way to render things in Tortuga, but let's start with something!
print render(contact_us)

# Out comes the HTML that we'd expect.  Notice the gadget has been rendered 
# where the container was.  A container can have lots of gadgets, and there's no
# artificial or silly requirements on what the html needs to look like.

# <html xmlns="http://www.w3.org/1999/xhtml">
#   <head><title>Contact Us</title></head>
#   <body><div class='gadget_basic_content' id='4'><p>Our Phone: 555-1212</p></div></body>
# </html>


# =======================================================================================
# This page won't be visible to the public until it's been 'approved' 
# through the workflow system
# =======================================================================================

# When a page (or any revisioned *thing*) is created, it gets set to "DRAFT" mode.
# Currently there's only three modes: 
#   DRAFT: for any new revision of anything
#   PENDING: it's been submitted for approval to the editor / manager / whatever
#   APPROVED: everybody gave it the 'ok'

# So how do we submit a page for approval?
contact_us.submit()

# Seriously, did you expect it to be any harder than this?
# Let's say we also have manager privileges and we get to approve this page.
# This time let's specify a comment for future people working on the site to see.
contact_us.approve("When we get an email address, we should include it here too!")

# Alternatively, the manager could have declined to publish the page, 
# setting it back to draft mode like so:
# contact_us.decline("That's the wrong phone number!")

# Let's make sure this is really getting saved to the database
session.flush()
session.refresh(contact_us)
assert contact_us.workflow_status == WORKFLOW_STATUS['APPROVED']


# =======================================================================================
# So I said this supported revisions, right?  Oh boy does it!!!
# =======================================================================================

# Let's say we want to change the url for the page we created... we could
# just change it directly:
# contact_us.path_name = u'contact'
# But I think what we really want is to revision this change so we can 'undo' it later:
contact_us_r2 = contact_us.new_revision(path_name=u'contact')
session.add(contact_us_r2) # IMPORTANT - don't forget to persist the new revision!

# No seriously, it's that simple!  We remember we have to submit this through the workflow
# system.  We're in a hurry though, and we're sure about this change - so we can skip
# the 'submit' process and just approve it right away:
contact_us_r2.approve()
session.flush()

# So what just happened?  If you query the database you'll now see two 'Page' objects,
# representing the two versions of the page that we created:
revisions = session.query(Page).all()
assert len(revisions) == 2
assert contact_us in revisions
assert contact_us_r2 in revisions

# Sometimes we'll just want to get the latest revision of our pages:
latest = session.query(Page).filter(Page.is_latest_version==True).all()
assert latest == [contact_us_r2]

# Sometimes we'll have a new revision that hasn't been Approved yet...
contact_us_r3 = contact_us_r2.new_revision(title=u"Call us Today!")
session.add(contact_us_r3)
session.flush()
# ... and we'll want to find the latest Approved version of the page:
latest_approved = session.query(Page).filter(Page.is_latest_approved_version==True).all()
assert latest_approved == [contact_us_r2]

# Another useful feature is scheduling things to be automatically published in the future:
tomorrow = datetime.datetime.today() + datetime.timedelta(days=1)
contact_us_r3.release_date = tomorrow
contact_us_r3.approve()
session.flush()

# So revision 3 is approved for publishing tomorrow, but in the meantime the website will show 
# the most recent approved version that's "effective" (meaning after a release date - if one 
# was set, and before an expiration date - if one was set):
active_versions = session.query(Page).filter(
    Page.is_latest_approved_effective_version==True
).all()
assert active_versions == [contact_us_r2]

# So just to review, we've gone over these terms:
# Approved - means a page got approved by the editor or whoever does this
# Effective - means that Page.release_date <= TODAY <= Page.expiration_date
# Latest - means this is the latest version of a Page, including any other criteria.
#
# On the admin site when you go to edit a page, you'll typically be looking at the LATEST
# version, with no other special criteria.  Your public website would show the
# LATEST APPROVED EFFECTIVE version of each page, with just one final criterion coming up...

# First lets make some more pages.  A website with just one page isn't very interesting, right!
homepage = Page(title=u"Welcome!", path_name=u"homepage", template=t2)
about_us = Page(title=u"About Us", path_name=u"about_us", template=t2)
# Might as well publish them
homepage.approve()
about_us.approve()
# Don't forget to save these to the database!
session.add_all([homepage, about_us])
session.flush()

# OK so someone over in sales said that the "About Us" page was very unpopular, and it
# was just totally ruining their sales, so they want us to remove that page from the site.
# We *could* just delete the page from the database:
# session.delete(about_us)
#
# But we want to be able to 'undo' this, so instead we create a new revision and mark 
# it as deleted.
about_us_r2 = about_us.new_revision(content_status=CONTENT_STATUS['DELETED'])
about_us_r2.approve()

# Since this is an example, we always remember to add our new revisions to the session.
# In real life, you won't always remember.  
session.add(about_us_r2)
session.flush()

# Here's how we'd get all the latest approved, effective, pages that aren't deleted:
live_versions = session.query(Page).filter(
    (Page.is_latest_approved_effective_version==True) & 
    (Page.content_status != CONTENT_STATUS['DELETED'])
).all()
assert set(live_versions) == set([contact_us_r2, homepage])

# This is the final criterion I mentioned just a little bit ago.  The pages that 
# will be "live" on the website are the pages whose 
# LATEST APPROVED EFFECTIVE revision is not DELETED.

# Don't worry - you don't have to memorize this, there will be plenty of shortcuts
# to get the "live" versions of stuff.  For example, try this on for size!
live_versions = session.query(Page).filter(Page.is_live==True).all() # sooo easy!
assert set(live_versions) == set([contact_us_r2, homepage])

# Ok but what if we want to 'undo' that deletion?  The opposite of creating a new
# revision is to restore an old version.  So we find the revision we want, and 'restore' it.
# In this case we want to 'restore' the revision just prior to the deletion:
about_us_r3 = about_us.new_revision(content_status=CONTENT_STATUS['RESTORED'])
about_us_r3.approve()
session.add(about_us_r3)
session.flush()

# So now the website should show the latest approved effective revisions of all
# three pages that we created today:
live_versions = session.query(Page).filter(Page.is_live==True).all()
assert set(live_versions) == set([contact_us_r2, homepage, about_us_r3])

# And here's another neat feature, you can easily see the history of any revisioned
# thing (pages, gadgets, etc), using the 'history' property:
assert set(about_us_r3.history) == set([about_us, about_us_r2])


# =======================================================================================
# So how about translations? Oh, you're going to like this!  A translation is just
# a revision with a different 'locale' value.  Check it out!
# =======================================================================================

# Making a new translation couldn't be easier!  Those pages we created earlier used
# the default locale ('en').  Of course, 'default locale' will be a configurable value.
# Anyways, let's make a spanish revision of our "Contact Us" page.
contact_us_spanish = contact_us_r2.new_revision(title=u"Contáctenos", locale='es')
contact_us_spanish.approve()
session.add(contact_us)
session.flush()

# So how do we query for the spanish pages?  Here's where SQLAlchemy really comes through
# for us.  We could construct a query to get ALL spanish revisions...
all_spanish_revisions = session.query(Page).filter(Page.locale=='es')
# Or we could ask for just the 'live' spanish revisions:
live_spanish_revisions = all_spanish_revisions.filter(Page.is_live==True)

# So here's where things get more... explicit.  When we were just dealing with one locale,
# everything was sunshine and bubblegum.  Now we're going to have to deal with some more
# of the machinery, and I have to tell you a secret.  The secret is that a Page
# is really composed of two different objects, Page.Localized and Page.Generic.  Can
# you guess what these do?  Well, some things like Page.title need to be localized,
# and other things like Page.parent_id absolutely need to be the same no matter which locale 
# we're interested in.  The most effecient way to store this data is to split up the
# localized stuff from the.. generic stuff.  Hence, Page.Localized and Page.Generic

# You can look at these objects yourself, they're available on all page instances:
assert isinstance(contact_us_spanish._page_localized, Page.Localized)
assert isinstance(contact_us_spanish._page_generic, Page.Generic)

# So when we created our spanish revision, the new_revision() method looked at those 
# keyword arguments we passed it, and because we only passed in localized values, 
# the new revision created a new Page.Localized object to hold these values, while using
# the existing Page.Generic object from the english version:
assert contact_us_spanish._page_generic is contact_us_r2._page_generic

# Did your head just explode?  I hope the concept isn't too difficult.  Is it?

# So when we want to change a generic value, such as Page.path_name (urls are not localized),
# we want that new Page.Generic object attached to all our translations.  Say that we want
# to change the path_name for our "Contact Us" page.  We want to shorten it up from
# 'contact_us' to just plain 'contact'.  Here's the long way to do it:
contact_us_spanish_r2 = contact_us_spanish.new_revision(path_name='contact')
contact_us_r3 = contact_us_r2.new_revision(_page_generic=contact_us_r2._page_generic)
contact_us_spanish_r2.approve()
contact_us_r3.approve()
session.add_all([contact_us_spanish_r2, contact_us_r3])
session.flush()

# Is that a lot to type?  You bet it is!  There will be a shortcut for this, don't worry.

# In the meantime though, let's make sure our changes come out of the database OK:
live_contact_translations = session.query(Page).filter(
    (Page.is_live==True) & (Page.content_id==contact_us_r3.content_id)
).all()

assert set(live_contact_translations) == set([contact_us_r3, contact_us_spanish_r2])


# =======================================================================================
# Conclusion
# =======================================================================================

# Hey, pat yourself on the back for getting this far!  We looked at how to make
# Pages and Templates, how to add Containers and Gadgets, and how to make new revisions
# and translations of these things and put them through the workflow process.

# Keep updating your svn for more recipes.  We're about to get some shortcuts for
# handling translations, and also a Tagging plugin so we can Tag our pages, which will 
# show Tortuga's ability to make a many-to-many revisioned, multi-lingual plugin.  
# Pretty cool, right!
# Answer: yes!